/*
* Copyright (C) 2008-2009 J-P Nurmi jpnurmi@gmail.com
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* $Id: messageparser.cpp 395 2009-05-07 21:13:25Z jpnurmi $
*/

#include "messageparser.h"
#include "alias.h"
#include <QTextStream>
#include <QMetaMethod>
#include <QSettings>
#include <QDateTime>
#include <QDebug>

QMap<QString, QString> MessageParser::MessageParserData::commands;

MessageParser::MessageParser(QObject* parent) : QObject(parent)
{
    initialize();
}

QStringList MessageParser::commands()
{
    return MessageParserData::commands.keys();
}

QStringList MessageParser::receivers() const
{
    return d.receivers;
}

void MessageParser::setReceivers(const QStringList& receivers)
{
    d.receivers = receivers;
}

void MessageParser::connectSlotsByName(const QObject* receiver)
{
    if (!receiver)
        return;
    const QMetaObject *thisMo = metaObject();
    const QMetaObject *thatMo = receiver->metaObject();
    Q_ASSERT(thisMo && thatMo);
    int thatOffset = thatMo->methodOffset();
    for (int j = thatOffset; j < thatMo->methodCount(); ++j) {
        QMetaMethod thatSlot = thatMo->method(j);
        const char* slotSignature = thatSlot.signature();
        Q_ASSERT(slotSignature);
        int thisOffset = thisMo->methodOffset();
        for (int i = thisOffset; i < thisMo->methodCount(); ++i) {
            QMetaMethod thisSignal = thisMo->method(i);
            if (thisSignal.parameterTypes() != thatSlot.parameterTypes())
                continue;
            if (thisSignal.methodType() == QMetaMethod::Signal) {
                const char* signalSignature = thisSignal.signature();
                Q_ASSERT(signalSignature);
                if (qstrcmp(slotSignature, signalSignature))
                    continue;
                QMetaObject::connect(this, i, receiver, j);
            }
        }
    }
}

void MessageParser::parse(const QString& message, const QString& defaultReceiver)
{
    if (message.startsWith('/'))
    {
        QString copy(message);
        copy.remove(0, 1); // skip '/'
        QTextStream stream(&copy, QIODevice::ReadOnly);
        QString command = parseWord(stream, AnyWord);

        Alias alias(command);
        if (alias.command() == "message" || alias.command() == "notice")
        {
            // message|notice <receiver> <message>
            QString receiver = parseWord(stream, AnyWord);
            QString message = copy.mid(stream.pos()).trimmed();

            if (!receiver.isNull() && !message.isNull())
            {
                if (alias.command() == "message")
                    emit cmdMessage(receiver, message);
                else
                    emit cmdNotice(receiver, message);
                return;
            }
        }
        if (alias.command() == "action")
        {
            // action <message>
            QString message = copy.mid(stream.pos()).trimmed();

            if (!message.isNull())
            {
                emit cmdCtcpAction(defaultReceiver, message);
                return;
            }
        }
        else if (alias.command() == "join")
        {
            // join <channel> (<key>)
            QString channel = parseWord(stream, AnyWord);
            QString key = parseWord(stream, AnyWord);

            if (!channel.isEmpty())
            {
                if (!channel.startsWith(QLatin1Char('#')) && !channel.startsWith(QLatin1Char('&')))
                    channel.prepend(QLatin1Char('#'));
                emit cmdJoin(channel, key);
                return;
            }
        }
        else if (alias.command() == "part")
        {
            // part (<channel>) (<reason>)
            QString channel = parseWord(stream, AnyReceiver, defaultReceiver);
            QString reason = copy.mid(stream.pos()).trimmed();

            emit closeView(channel, reason);
            return;
        }
        else if (alias.command() == "ctcp")
        {
            // ctcp <receiver> <request>
            QString receiver = parseWord(stream, AnyWord);
            QString request = copy.mid(stream.pos()).trimmed();

            if (!request.compare("PING", Qt::CaseInsensitive))
                request = QString("%1 %2").arg(request).arg(QDateTime::currentDateTime().toTime_t());

            if (!receiver.isNull() && !request.isNull())
            {
                emit cmdCtcpRequest(receiver, request);
                return;
            }
        }
        else if (alias.command() == "names")
        {
            // names (<channels>)
            QStringList channels = parseWords(stream, Channel, defaultReceiver);

            foreach (const QString& channel, channels)
                emit cmdNames(channel);
            return;
        }
        else if (alias.command() == "whois")
        {
            // whois <users>
            QStringList users = parseWords(stream, AnyWord);

            if (!users.isEmpty())
            {
                foreach (const QString& user, users)
                    emit cmdWhois(user);
                return;
            }
        }
        else if (alias.command() == "invite")
        {
            // invite <users> <channel>
            QStringList params = parseWords(stream, AnyWord);
            if (params.count() > 1)
            {
                QString channel = params.last();
                for (int i = 0; i < params.count() - 1; ++i)
                    emit cmdInvite(params.at(i), channel);
                return;
            }
        }
        else if (alias.command() == "topic")
        {
            // topic (<channel>) (<topic>)
            QString channel = parseWord(stream, Channel, defaultReceiver);
            QString topic = copy.mid(stream.pos()).trimmed();

            emit cmdTopic(channel, topic);
            return;
        }
        else if (alias.command() == "query")
        {
            // query <users>
            QStringList users = parseWords(stream, AnyWord);

            if (!users.isEmpty())
            {
                foreach (const QString& user, users)
                    emit openView(user);
                return;
            }
        }
        else if (alias.command() == "clear")
        {
            // clear <views>
            QStringList views = parseWords(stream, AnyReceiver, defaultReceiver);

            foreach (const QString& view, views)
                emit clearView(view);
            return;
        }
        else if (alias.command() == "time" || alias.command() == "version" || alias.command() == "ping")
        {
            // time|version|ping (<receivers>)
            QStringList receivers = parseWords(stream, AnyWord);

            if (receivers.isEmpty())
                emit sendRaw(alias.command());
            else
                foreach (const QString& receiver, receivers)
                    parse(QString("/ctcp %1 %2").arg(receiver).arg(alias.command()), defaultReceiver);
            return;
        }
        else if (alias.command() == "mode")
        {
            // mode <receiver> <modes> (<limit>) (<targets>)
            QString rest = copy.mid(stream.pos()).trimmed();
            if (rest.isEmpty())
                rest = defaultReceiver;
            emit sendRaw(QString("%1 %2").arg("MODE").arg(rest));
            return;
        }
        else if (alias.command() == "op" || alias.command() == "deop" || alias.command() == "voice" || alias.command() == "devoice")
        {
            QStringList params = parseWords(stream, AnyWord);
            QString channel = params.value(0);
            QStringList nicks = params.mid(1);
            if (!channel.startsWith(QLatin1Char('#')) && !channel.startsWith(QLatin1Char('&')))
            {
                channel = defaultReceiver;
                nicks = params;
            }

            QString mode;
            if (alias.command() == "op")
                mode = "+o";
            else if (alias.command() == "deop")
                mode = "-o";
            else if (alias.command() == "voice")
                mode = "+v";
            else if (alias.command() == "devoice")
                mode = "-v";

            if (!channel.isEmpty() && !nicks.isEmpty())
            {
                foreach (const QString& nick, nicks)
                    emit cmdMode(channel, QString("%1 %2").arg(mode).arg(nick));
                return;
            }
        }
        else if (alias.command() == "ban")
        {
            QString rest = copy.mid(stream.pos()).trimmed();
            if (!rest.isEmpty())
            {
                parse(QString("/mode %1 +b %2").arg(defaultReceiver).arg(rest), defaultReceiver);
                return;
            }
        }
        else if (alias.command() == "unban")
        {
            QString rest = copy.mid(stream.pos()).trimmed();
            if (!rest.isEmpty())
            {
                parse(QString("/mode %1 -b %2").arg(defaultReceiver).arg(rest), defaultReceiver);
                return;
            }
        }
        else if (alias.command() == "kick" || alias.command() == "kickban")
        {
            QString rest = copy.mid(stream.pos()).trimmed();
            int index = rest.indexOf(QRegExp("\\s"));
            QString channel = rest.left(index);
            if (!channel.startsWith(QLatin1Char('#')) && !channel.startsWith(QLatin1Char('&')))
                channel = defaultReceiver;
            else
                rest = rest.mid(index + 1).trimmed();

            index = rest.indexOf(QRegExp("\\s"));
            QString nick = rest.left(index);
            if (index == -1)
                rest = QString();
            else
                rest = rest.mid(index + 1).trimmed();

            bool ban = alias.command() == "kickban";
            if (!channel.isEmpty() && !nick.isEmpty())
            {
                if (ban)
                    parse(QString("/ban %1").arg(nick), channel);
                emit cmdKick(nick, channel, rest);
                return;
            }
        }
        else if (alias.command() == "nick")
        {
            // nick <nick>
            QString nick = parseWord(stream, AnyWord);
            if (!nick.isEmpty())
            {
                emit setNick(nick);
                return;
            }
        }
        else if (alias.command() == "raw")
        {
            QString rest = copy.mid(stream.pos()).trimmed();
            emit sendRaw(rest.toUtf8());
            return;
        }
        else if (alias.command() == "connect")
        {
            QStringList params = parseWords(stream, AnyWord);
            QString host = params.value(0);
            QString pass;
            QString nick;
            bool ok = false;
            qint16 port = params.value(1).toUInt(&ok);
            if (!ok)
            {
                port = 6667;
                pass = params.value(1);
                nick = params.value(2);
            }
            else
            {
                pass = params.value(2);
                nick = params.value(3);
            }

            emit connectTo(host, port, pass, nick);
            return;
        }
        else if (alias.command() == "disconnect")
        {
            QString rest = copy.mid(stream.pos()).trimmed();
            emit disconnectFrom(rest);
            return;
        }
        else if (alias.command() == "quit")
        {
            QString rest = copy.mid(stream.pos()).trimmed();
            emit quit(rest);
            return;
        }
        else if (alias.command() == "help")
        {
            QStringList commands = parseWords(stream, AnyWord);
            if (!commands.isEmpty())
            {
                foreach (const QString& command, commands)
                    helper(command);
                return;
            }
            helper();
            return;
        }
        helper(alias);
    }
    else
    {
        emit cmdMessage(defaultReceiver, message);
    }
}

void MessageParser::helper(const Alias& alias)
{
    if (alias.isNull())
    {
        QMapIterator<QString, QString> it(d.commands);
        while (it.hasNext())
        {
            it.next();
            emit cmdHelp(it.value().arg(it.key().toUpper()));
        }
    }
    else
    {
        QString text = d.commands.value(alias.command(), "%1: unknown command");
        emit cmdHelp(text.arg(alias.name().toUpper()));
    }
}

QString MessageParser::parseWord(QTextStream& stream, WordFlags flags, const QString& defaultValue) const
{
    // save pos
    qint64 pos = stream.pos();

    QString word;
    stream >> word;

    if (flags & AnyWord)
    {
        return word;
    }
    if (flags & Receiver)
    {
        QString tmp;
        if (tmp.startsWith(QLatin1Char('#')) || tmp.startsWith(QLatin1Char('&')))
            tmp.remove(0, 1);

        if (d.receivers.contains(tmp, Qt::CaseInsensitive))
            return word;
    }
    if (flags & Prefixed)
    {
        if (word.startsWith(QLatin1Char('+')) || word.startsWith(QLatin1Char('-')))
            return word;
    }
    if (flags & Channel)
    {
        if (d.receivers.contains(word, Qt::CaseInsensitive))
        {
            return word;
        }
        else if (!word.startsWith(QLatin1Char('#')) && !word.startsWith(QLatin1Char('&')))
        {
            if (d.receivers.contains(QLatin1Char('#') + word, Qt::CaseInsensitive)
                || d.receivers.contains(QLatin1Char('&') + word, Qt::CaseInsensitive))
            {
                return word;
            }
        }
    }
    if (flags & Number)
    {
        bool ok = false;
        word.toInt(&ok);
        if (ok)
            return word;
    }

    // not found, seek back
    stream.seek(pos);
    return defaultValue;
}

QStringList MessageParser::parseWords(QTextStream& stream, WordFlags flags, const QString& defaultValue) const
{
    QStringList words;
    QString word;
    while (!(word = parseWord(stream, flags)).isNull())
        words += word;
    if (words.isEmpty())
    {
        if (!(flags & Channel) || defaultValue.startsWith(QLatin1Char('#')) || defaultValue.startsWith(QLatin1Char('&')))
            words += defaultValue;
    }
    return words;
}

void MessageParser::initialize()
{
    QSettings settings;
    settings.beginGroup("Aliases");
    if (settings.allKeys().isEmpty())
    {
        // first run
        Alias::add("j", "join");
        Alias::add("leave", "part");
        Alias::add("close", "part");
        Alias::add("p", "part");
        Alias::add("q", "query");
        Alias::add("exit", "quit");
        Alias::add("w", "whois");
        Alias::add("who", "names");
        Alias::add("msg", "message");
        Alias::add("t", "topic");
        Alias::add("me", "action");
        Alias::add("clr", "clear");
        Alias::add("c", "clear");
        Alias::add("k", "kick");
        Alias::add("b", "ban");
        Alias::add("ub", "unban");
        Alias::add("kb", "kickban");
        Alias::add("h", "help");
        Alias::add("?", "help");
    }

    if (MessageParserData::commands.isEmpty())
    {
        MessageParserData::commands.insert("action", "%1 (&lt;receiver&gt;) &lt;message&gt;");
        MessageParserData::commands.insert("ban", "%1 &lt;users&gt;");
        MessageParserData::commands.insert("clear", "%1");
        MessageParserData::commands.insert("connect", "%1 (&lt;host&gt; &lt;port&gt; &lt;password&gt; &lt;nick&gt;)");
        MessageParserData::commands.insert("ctcp", "%1 (&lt;receiver&gt;) &lt;request&gt;");
        MessageParserData::commands.insert("deop", "%1 &lt;users&gt;");
        MessageParserData::commands.insert("devoice", "%1 &lt;users&gt;");
        MessageParserData::commands.insert("disconnect", "%1 (&lt;message&gt;)");
        MessageParserData::commands.insert("help", "%1 (&lt;command&gt;)");
        MessageParserData::commands.insert("invite", "%1 &lt;users&gt; &lt;channel&gt;");
        MessageParserData::commands.insert("join", "%1 &lt;channel&gt; (&lt;key&gt;)");
        MessageParserData::commands.insert("kick", "%1 (&lt;channel&gt;) &lt;user&gt; (&lt;reason&gt;)");
        MessageParserData::commands.insert("kickban", "%1 (&lt;channel&gt;) &lt;user&gt; (&lt;reason&gt;)");
        MessageParserData::commands.insert("message", "%1 (&lt;receiver&gt;) &lt;message&gt;");
        MessageParserData::commands.insert("mode", "%1 &lt;channel&gt; (&lt;mode&gt; &lt;users&gt;)");
        MessageParserData::commands.insert("names", "%1 (&lt;channels&gt;)");
        MessageParserData::commands.insert("nick", "%1 &lt;nick&gt;");
        MessageParserData::commands.insert("notice", "%1 (&lt;receiver&gt;) &lt;message&gt;");
        MessageParserData::commands.insert("op", "%1 &lt;users&gt;");
        MessageParserData::commands.insert("part", "%1 (&lt;channel&gt;) (&lt;message&gt;)");
        MessageParserData::commands.insert("ping", "%1 &lt;user&gt;");
        MessageParserData::commands.insert("query", "%1 &lt;user&gt;");
        MessageParserData::commands.insert("quit", "%1 (&lt;message&gt;)");
        MessageParserData::commands.insert("raw", "%1 (&lt;message&gt;)");
        MessageParserData::commands.insert("time", "%1 (&lt;users&gt;)");
        MessageParserData::commands.insert("topic", "%1 (&lt;channel&gt;) (&lt;topic&gt;)");
        MessageParserData::commands.insert("unban", "%1 &lt;users&gt;");
        MessageParserData::commands.insert("version", "%1 (&lt;users&gt;)");
        MessageParserData::commands.insert("voice", "%1 &lt;users&gt;");
        MessageParserData::commands.insert("whois", "%1 &lt;user&gt;");
    }
}
